using System;
using System.ComponentModel;
using System.Runtime.Serialization;

using SharpGL.Media3D;
using SharpGL.Enumerations;
using SharpGL.SceneGraph.Lighting;

namespace SharpGL.SceneGraph.Assets
{
    /// <summary>
    /// a material object is defined in mathematical terms, i.e it's not exclusivly 
    /// for OpenGL. This means later on, DirectX or custom library functions could
    /// be added.
    /// </summary>
    [TypeConverter(typeof(System.ComponentModel.ExpandableObjectConverter))]
    [DataContract(/*IsReference = true, */Name = "Material", Namespace = "SharpGL.SceneGraph")]
    public class Material : Asset, IBindable
    {
        #region Private Consts

        private static readonly string defaultName = "Material";
        private static readonly FaceMode defaultFaceMode = FaceMode.FrontAndBack;
        private static readonly GLColor defaultAmbient = new GLColor(0.2f, 0.2f, 0.2f, 1.0f);
        private static readonly GLColor defaultDiffuse = new GLColor(0.8f, 0.8f, 0.8f, 1.0f);
        private static readonly GLColor defaultSpecular = new GLColor(0.0f, 0.0f, 0.0f, 1.0f);
        private static readonly GLColor defaultEmission = new GLColor(0.1f, 0.1f, 0.1f, 1.0f);
        private static readonly float defaultShininess = 0;
        private static readonly Texture2D defaultTexture = null;

        #endregion Private Consts

        #region Private Fields

        /// <summary>
        /// Face mode.
        /// </summary>
        [DataMember()]
        private FaceMode faceMode;

        /// <summary>
        /// Ambient color.
        /// </summary>
        [DataMember()]
        private GLColor ambient;

        /// <summary>
        /// Diffuse color.
        /// </summary>
        [DataMember()]
        private GLColor diffuse;

        /// <summary>
        /// Specular color.
        /// </summary>
        [DataMember()]
        private GLColor specular;

        /// <summary>
        /// Emission.
        /// </summary>
        [DataMember()]
        private GLColor emission;

        /// <summary>
        /// Shininess.
        /// </summary>
        [DataMember()]
        private float shininess;

        /// <summary>
        /// The texture.
        /// </summary>
        [DataMember()]
        private Texture2D texture;

        #endregion Private Fields

        #region Private Methods

        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            NotifyPropertyChanged(sender.GetType() + "." + e.PropertyName);
        }

        #endregion Private Methods

        #region Public Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Material"/> class.
        /// </summary>
        public Material()
        {
            Name = (defaultName == null ? "" : (string)defaultName.Clone());
            faceMode = defaultFaceMode;
            ambient = defaultAmbient;
            diffuse = defaultDiffuse;
            specular = defaultSpecular;
            emission = defaultEmission;
            shininess = defaultShininess;
            texture = (defaultTexture == null ? defaultTexture : (Texture2D)defaultTexture.Clone());
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// Calculates the lighting.
        /// </summary>
        /// <param name="light">The light.</param>
        /// <param name="angle">The angle.</param>
        /// <returns></returns>
        public GLColor CalculateLighting(Light light, float angle)
        {
            double angleRadians = angle * Math.PI / 180.0;
            GLColor reflected = this.ambient * light.Ambient;
            reflected += this.diffuse * light.Diffuse * (float)Math.Cos(angleRadians);

            return reflected;
        }

        /// <summary>
        /// Pushes this object into the provided OpenGL instance.
        /// This will generally push elements of the attribute stack
        /// and then set current values.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public virtual void Push(OpenGL gl)
        {
            if (OpenGL.IsValidContext(gl))
            {
                // Push lighting and texture attributes.
                gl.PushAttrib(AttributeMask.Lighting | AttributeMask.Texture);
            }

            // Bind the material.
            Bind(gl);
        }

        /// <summary>
        /// Pops this object from the provided OpenGL instance.
        /// This will generally pop elements of the attribute stack,
        /// restoring previous attribute values.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public virtual void Pop(OpenGL gl)
        {
            if (OpenGL.IsValidContext(gl))
            {
                // Pop lighting attributes.
                gl.PopAttrib();
            }
        }

        /// <summary>
        /// Bind to the specified OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public void Bind(OpenGL gl)
        {
            if (OpenGL.IsValidContext(gl))
            {
                byte[] isLightingEnable = new byte[1];
                gl.GetBooleanv(Enumerations.GetTarget.Lighting, isLightingEnable);
                if (isLightingEnable[0] != 0)
                {
                    //	Set the material properties.
                    gl.Material((uint)this.faceMode, OpenGL.GL_AMBIENT, this.ambient);
                    gl.Material((uint)this.faceMode, OpenGL.GL_DIFFUSE, this.diffuse);
                    gl.Material((uint)this.faceMode, OpenGL.GL_SPECULAR, this.specular);
                    gl.Material((uint)this.faceMode, OpenGL.GL_EMISSION, this.emission);
                    gl.Material((uint)this.faceMode, OpenGL.GL_SHININESS, this.shininess);
                }
                else
                {
                    gl.Color(this.diffuse);
                }
            }

            //  If we have a texture, bind it.
            //  No need to push or pop it as we do that earlier.
            if (this.texture != null)
                this.texture.Bind(gl);
        }

        #endregion Public Methods

        #region Properties

        /// <summary>
        /// Gets or sets the ambient.
        /// </summary>
        /// <value>
        /// The ambient.
        /// </value>
        [Description("Face mode."), Category("Material")]
        public FaceMode FaceMode
        {
            get { return faceMode; }
            set
            {
                if (this.faceMode != value)
                {
                    this.faceMode = value;

                    NotifyPropertyChanged("FaceMode");
                }
            }
        }

        /// <summary>
        /// Gets or sets the ambient.
        /// </summary>
        /// <value>
        /// The ambient.
        /// </value>
        [Description("Ambient color."), Category("Material")]
        public GLColor Ambient
        {
            get { return ambient; }
            set
            {
                if (this.ambient != value)
                {
                    this.ambient = value;

                    NotifyPropertyChanged("Ambient");
                }
            }
        }

        /// <summary>
        /// Gets or sets the diffuse.
        /// </summary>
        /// <value>
        /// The diffuse.
        /// </value>
        [Description("Diffuse color."), Category("Material")]
        public GLColor Diffuse
        {
            get { return diffuse; }
            set
            {
                if (this.diffuse != value)
                {
                    this.diffuse = value;

                    NotifyPropertyChanged("Diffuse");
                }
            }
        }

        /// <summary>
        /// Gets or sets the specular.
        /// </summary>
        /// <value>
        /// The specular.
        /// </value>
        [Description("Specular color."), Category("Material")]
        public GLColor Specular
        {
            get { return specular; }
            set
            {
                if (this.specular != value)
                {
                    this.specular = value;

                    NotifyPropertyChanged("Specular");
                }
            }
        }

        /// <summary>
        /// Gets or sets the emission.
        /// </summary>
        /// <value>
        /// The emission.
        /// </value>
        [Description("Emission color."), Category("Material")]
        public GLColor Emission
        {
            get { return emission; }
            set
            {
                if (this.emission != value)
                {
                    this.emission = value;

                    NotifyPropertyChanged("Emission");
                }
            }
        }

        /// <summary>
        /// Gets or sets the shininess.
        /// </summary>
        /// <value>
        /// The shininess.
        /// </value>
        [Description("Shininess."), Category("Material")]
        public float Shininess
        {
            get { return shininess; }
            set
            {
                if (this.shininess != value)
                {
                    this.shininess = value;

                    NotifyPropertyChanged("Shininess");
                }
            }
        }

        /// <summary>
        /// Gets or sets the texture.
        /// </summary>
        /// <value>
        /// The texture.
        /// </value>
        [Description("The Material's Texture."), Category("Material")]
        public Texture2D Texture
        {
            get { return texture; }
            set
            {
                if (this.texture != value)
                {
                    if (this.texture != null)
                        texture.PropertyChanged -= new PropertyChangedEventHandler(OnPropertyChanged);

                    this.texture = value;

                    if (this.texture != null)
                        texture.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);

                    NotifyPropertyChanged("Texture");
                }
            }
        }

        #endregion
    }
}

